<?php
/**
 * I know no such things as genius,it is nothing but labor and diligence.
 *
 * @copyright (c) 2015~2019 BD All rights reserved.
 * @license http://www.apache.org/licenses/LICENSE-2.0
 * @author BD<657306123@qq.com>
 */

namespace app;

use app\common\exception\WechatException;
use think\Container;
use think\db\exception\DataNotFoundException;
use think\db\exception\ModelNotFoundException;
use think\exception\Handle;
use think\exception\HttpException;
use think\exception\HttpResponseException;
use think\exception\ValidateException;
use think\facade\Config;
use think\facade\Request;
use think\Response;
use Xin\Auth\AuthenticationException;
use Xin\Thinkphp5\Hint\Facade\Hint;

/**
 * 接管异常处理
 */
class ExceptionHandle extends Handle{

	use NotFoundModelExceptionHandle;

	/**
	 * 不需要记录信息（日志）的异常类列表
	 *
	 * @var array
	 */
	protected $ignoreReport = [
		HttpException::class,
		HttpResponseException::class,
		ModelNotFoundException::class,
		DataNotFoundException::class,
		ValidateException::class,
		AuthenticationException::class,
	];

	/**
	 * 预处理异常
	 *
	 * @param \Exception $e
	 * @return \Exception
	 */
	private function prepareException(\Exception $e){
		if($e instanceof ModelNotFoundException
			|| $e instanceof DataNotFoundException){
			return new HttpException(
				404, $this->resolveModelErrorMessage($e), $e
			);
		}

		return $e;
	}

	/**
	 * 异常处理
	 *
	 * @param \Exception $e
	 * @return \think\Response
	 */
	public function render(\Exception $e){
		$e = $this->prepareException($e);

		// 参数验证错误
		if($e instanceof ValidateException || $e instanceof WechatException){
			return $this->isAjax() ? Hint::error($e) : $this->hint($e);
		}elseif($e instanceof AuthenticationException){
			return $this->authenticHandle($e);
		}

		// 其他错误交给系统处理
		return parent::render($e);
	}

	/**
	 * @access protected
	 * @param HttpException $e
	 * @return Response
	 */
	protected function renderHttpException(HttpException $e){
		$statusCode = $e->getStatusCode();

		if($this->isAjax()){
			$msg = $e->getMessage();
			if(empty($msg)){
				$msg = strval($statusCode);
			}

			return Hint::error($msg, $statusCode);
		}

		if(in_array($statusCode, [403, 404])){
			$this->hint($e);
		}

		return parent::renderHttpException($e);
	}

	/**
	 * @access protected
	 * @param \Exception $exception
	 * @return Response
	 */
	protected function convertExceptionToResponse(\Exception $exception){
		if(!$this->isAjax()){
			return parent::convertExceptionToResponse($exception);
		}

		// 收集异常数据
		$code = $this->getCode($exception);
		$msg = $this->getMessage($exception);

		/** @var \think\App $app */
		$app = Container::get('app');

		// 不显示详细错误信息
		if(!$app->isDebug() && !$app->config('show_error_msg')){
			$msg = Container::get('app')->config('error_message');
		}

		// 调试模式，获取详细的错误信息
		$extend = $app->isDebug() ? [
			'name'   => get_class($exception),
			'file'   => $exception->getFile(),
			'line'   => $exception->getLine(),
			'trace'  => $exception->getTrace(),
			'source' => $this->getSourceCode($exception),
			'datas'  => $this->getExtendData($exception),
			'tables' => [
				'GET Data'              => $_GET,
				'POST Data'             => $_POST,
				'Files'                 => $_FILES,
				'Cookies'               => $_COOKIE,
				'Session'               => isset($_SESSION) ? $_SESSION : [],
				'Server/Request Data'   => $_SERVER,
				'Environment Variables' => $_ENV,
			],
		] : [];

		return Hint::error($msg, $code, null, $extend);
	}

	/**
	 * Report or log an exception.
	 *
	 * @access public
	 * @param \Exception $exception
	 * @return void
	 */
	public function report(\Exception $exception){
		if(!$this->isIgnoreReport($exception)){
			// 收集异常数据
			$data = [
				'file'    => $exception->getFile(),
				'line'    => $exception->getLine(),
				'message' => $this->getMessage($exception),
				'code'    => $this->getCode($exception),
			];
			$log = "[{$data['code']}]{$data['message']}[{$data['file']}:{$data['line']}]";

			if(Container::get('app')->config('log.record_trace')){
				$log .= "\r\n".$exception->getTraceAsString();
			}

			Container::get('log')->record($log, 'error');
		}
	}

	/**
	 * 未授权处理
	 *
	 * @param \Xin\Auth\AuthenticationException $e
	 * @return \Symfony\Component\HttpFoundation\Response|\think\Response
	 */
	private function authenticHandle(AuthenticationException $e){
		$config = $e->getConfig();
		$url = $config['guest_url'] ?? '';
		if($this->isAjax()){
			return Hint::error("登录已失效", -1, $url);
		}else{
			return redirect($url);
		}
	}

	/**
	 * 是否Ajax请求
	 *
	 * @return bool
	 */
	private function isAjax(){
		return Request::module() === 'api' || Request::isAjax();
	}

	/**
	 * 错误输出
	 *
	 * @param \Exception $e
	 * @return Response
	 */
	private function hint(\Exception $e){
		$url = 'javascript:history.back(-1);';

		$code = $e->getCode();
		if($code == 0 && $e instanceof ValidateException){
			$code = 400;
		}

		$msg = $e->getMessage();

		$result = [
			'code' => $code,
			'msg'  => $msg,
			'data' => [],
			'url'  => $url,
			'wait' => 3,
			'time' => Request::time(),
		];

		return Response::create($result, 'jump')->options([
			'jump_template' => Config::get('dispatch_error_tmpl'),
		]);
	}
}
